/**
 * com.planet_ink.coffee_mud.core.intermud.i3.net.Interactive
 * Copyright (c) 1996 George Reese
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  	  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * The Imaginary interactive implementation
 * of interactive connections.
 */

package com.planet_ink.coffee_mud.core.intermud.i3.net;

import com.planet_ink.coffee_mud.core.intermud.i3.server.I3Server;
import com.planet_ink.coffee_mud.core.intermud.i3.server.ServerUser;
import com.planet_ink.coffee_mud.core.*;
import com.planet_ink.coffee_mud.core.collections.*;

import java.io.*;
import java.net.Socket;
import java.util.Date;
import java.util.List;
import java.util.Vector;

/**
 * This class provides an implementation of the Imaginary server
 * interactive module.  It is responsible for handling the login
 * of an individual user and processing its input as directed
 * by the server.
 * Created: 27 September 1996
 * Last modified: 27 September 1996
 * @author George Reese (borg@imaginary.com)
 * @version 1.0
 */
@SuppressWarnings({"unchecked","rawtypes"})
public abstract class Interactive implements ServerUser {
	/**
	 * Given a user name, this method will build a unique
	 * key.  This unique key has nothing to do with the
	 * unique object id.  The idea behind a key name is to
	 * ensure that you do not end up with a user named
	 * Descartes and a user named Des Cartes.  It removes
	 * all non-alphabetic characters and makes the string
	 * lower case.
	 * @exception InvalidNameException thrown if the name produces an unuseable key
	 * @param nom the visual name to create a key from
	 * @return the new key
	 * @throws InvalidNameException an error telling you to pick a new name
	 */
	static public String createKeyName(String nom) throws InvalidNameException 
	{
		final StringBuffer buff = new StringBuffer(nom.toLowerCase());
		String key = "";
		int i;

		for(i=0; i<buff.length(); i++)
		{
			final char c = buff.charAt(i);

			if( c >= 'a' && c <= 'z' )
			{
				key = key + c;
			}
			else if( c != '\'' && c != '-' && c != ' ' )
			{
				throw new InvalidNameException(c + " is an invalid character for names.");
			}
		}
		if( key.length() < 3 )
		{
			throw new InvalidNameException("Your name must have at least three alphabetic characters.");
		}
		else if( key.length() > 29 )
		{
			throw new InvalidNameException("Your name is too long.");
		}
		return key;
	}

	/**
	 * Given a user name, this method will find the Interactive
	 * instance associated with that user name.  If no such user is
	 * currently logged in, this method will return null.
	 * @param nom the name of the desired user
	 * @return the Interactive object for the specified name or null if no such user exists
	 */
	static public Interactive findUser(String nom)
	{
		final ServerUser[] users = I3Server.getInteractives();
		int i;

		try
		{
			nom = createKeyName(nom);
		}
		catch( final InvalidNameException e )
		{
			return null;
		}
		for(i=0; i<users.length; i++)
		{
			final Interactive user = (Interactive)users[i];

			if( user.getKeyName().equals(nom) )
			{
				return user;
			}
		}
		return null;
	}

	private InteractiveBody body;
	private Date			current_login_time;
	private boolean 		destructed;
	private String  		display_name;
	private String  		email;
	private InputThread 	input_thread;
	private String  		key_name;
	private Date			last_command_time;
	private String  		last_login_site;
	private Date			last_login_time;
	private String  		object_id;
	private PrintStream 	output_stream;
	private String  		password;
	private String  		real_name;
	private final Vector  	redirect;
	private Socket  		socket;

	/**
	 * Constructs a new interactive object and initializes
	 * its data.
	 */
	public Interactive()
	{
		super();
		destructed = false;
		input_thread = null;
		object_id = null;
		output_stream = null;
		redirect = new Vector();
	}

	/**
	 * Implementation of the ServerUser connect method.
	 * A mudlib will want to display a welcome screen
	 * and ask for a user name by extending this method.
	 * Here, the login time is set.
	 */
	@Override
	public synchronized void connect()
	{
		current_login_time = new Date();
		last_command_time = new Date();
	}

	/**
	 * Stops any running I/O threads for this interactive, closes the
	 * user socket, and marks the object for destruction according to
	 * the requirements of the ServerObject interface.
	 * @see com.planet_ink.coffee_mud.core.intermud.i3.server.ServerObject#getDestructed
	 */
	@Override
	public synchronized void destruct()
	{
		output_stream.flush();
		if( input_thread != null )
		{
			input_thread.stop();
			input_thread = null;
		}
		try
		{
			if(socket!=null)
				socket.close();
		}
		catch( final java.io.IOException e )
		{
			Log.errOut("IMInteractive",e);
		}
		destructed = true;
	}

	/**
	 * Called whenever a command is pulled off the incoming
	 * command stack.  If there is an instance of the Input
	 * class to which input is supposed to be redirected,
	 * then the command is sent there.  Otherwise it is sent
	 * to the parser.  Muds wishing to implement their own
	 * parser system should
	 * @param cmd the command to be executed
	 * @see #processInput
	 */
	protected synchronized void input(String cmd)
	{
		Input ob = null;

		if( redirect.size() > 0 )
		{
			ob = (Input)redirect.elementAt(0);
			redirect.removeElementAt(0);
		}
		if( ob != null )
		{
			ob.input(this, cmd);
		}
		else if( body != null )
		{
			body.executeCommand(cmd);
		}
		if( redirect.size() < 1 )
		{
			sendMessage(getPrompt(), true);
		}
	}

	/**
	 * This method is triggered by the input thread when it detects
	 * that the user has lost their link.  It will tell the body object
	 * that the link is lost, then destruct itself.
	 */
	protected void loseLink()
	{
		socket = null;
		if( body != null )
		{
			body.loseLink();
		}
		destruct();
	}

	/**
	 * Does event handling for the user object.  Each
	 * server cycle, the server triggers this method.  If
	 * the user has periodic events which occur to it,
	 * the event processor will flag that the event() method
	 * should be called.
	 * @see com.planet_ink.coffee_mud.core.intermud.i3.server.ServerObject#processEvent
	 */
	@Override
	public void processEvent()
	{
	}

	/**
	 * The server triggers this method once each server cycle to see
	 * if the user has any input waiting to be processed.  This method
	 * checks the input queue.  If there is input waiting, it updates
	 * the last command time and calls the input() method with the
	 * waiting command.  Otherwise it simply returns.
	 * @see #input
	 * @see com.planet_ink.coffee_mud.core.intermud.i3.server.ServerUser#processInput
	 * @see com.planet_ink.coffee_mud.core.intermud.i3.server.ServerThread#tick(com.planet_ink.coffee_mud.core.interfaces.Tickable, int)
	 */
	@Override
	public synchronized final void processInput()
	{
		if( input_thread != null )
		{
			final String msg = input_thread.nextMessage();

			if( msg != null )
			{
				last_command_time = new java.util.Date();
				input(msg);
			}
		}
	}

	/**
	 * Redirects user input to the input object passed to it.
	 * This will create a LIFO chain of input redirection.  For
	 * example, if I have my input currently redirected to a
	 * mud created editor, then I wish to get help from inside
	 * the editor, my next input will be directed to the help
	 * prompt.  If I enter something at that point with no further
	 * input redirection, my next input will then go back to the
	 * editor.
	 * @param ob the instance of com.planet_ink.coffee_mud.core.intermud.i3.net.Input to which input will be redirected
	 * @see com.planet_ink.coffee_mud.core.intermud.i3.net.Input
	 * @see #input
	 */
	public synchronized final void redirectInput(Input ob)
	{
		redirect.addElement(ob);
	}

	/**
	 * Sends a message across to the client with a newline appended
	 * to the message.
	 * @param msg the message to send to the client machine
	 */
	public final void sendMessage(String msg)
	{
		if( socket == null )
		{
			return;
		}
		sendMessage(msg, false);
	}

	/**
	 * Sends a message across to the client.  It will append
	 * nowrap is true, no newline will be appended.
	 * @param msg the message to send to the client
	 * @param nowrap if true, no newline is attached
	 */
	public final void sendMessage(String msg, boolean nowrap)
	{
		if( !nowrap )
		{
			msg += "\n";
		}
		output_stream.print(msg);
		output_stream.flush();
	}

	/**
	 * Validates a user password against a random string.
	 * @param other the password to check
	 * @return true if the two passwords match
	 */
	public final boolean validatePassword(String other)
	{
		return other.equals(password);
	}

	/**
	 * Provides the address from which this user is connected.
	 * @return the host name for this user's current site
	 */
	public final String getAddressName()
	{
		if(CMProps.getVar(CMProps.Str.MUDDOMAIN).length()>0)
			return CMProps.getVar(CMProps.Str.MUDDOMAIN).toLowerCase();
		return socket.getInetAddress().getHostName();
	}

	/**
	 * Provides the body to which this user is connected.
	 * @return the body to which this user is connected, or null if no body exists
	 */
	public final InteractiveBody getBody()
	{
		return body;
	}

	/**
	 * Sets the body to which this interactive connection
	 * is connected.  Any mudlib using this system for
	 * interactive management must implement the InteractiveBody
	 * interface for any body to be used by a user.
	 * @param ob the body to which this interactive is being connected
	 * @see com.planet_ink.coffee_mud.core.intermud.i3.net.InteractiveBody
	 */
	public void setBody(InteractiveBody ob)
	{
		body = ob;
	}

	/**
	 * Provides the time at which the user logged in for this session
	 * @return the time of login for the current session
	 */
	public final Date getCurrentLoginTime()
	{
		return current_login_time;
	}

	/**
	 * Tells whether or not the user is marked for destruction.
	 * @return true if the user is marked for destruction
	 */
	@Override
	public boolean getDestructed()
	{
		return destructed;
	}

	/**
	 * Provides the user's name as they wish it to appear
	 * with mixed capitalization, spaces, hyphens, etc.
	 * @return the user's display name
	 */
	public String getDisplayName()
	{
		return display_name;
	}

	/**
	 * Sets the user's display name.  Prevents the operation
	 * if the display name is not a permutation of the key
	 * name.
	 * @param str the new display name
	 */
	public final void setDisplayName(String str)
	{
		try
		{
			if( !getKeyName().equals(Interactive.createKeyName(str)) )
			{
				return;
			}
			display_name = str;
		}
		catch( final InvalidNameException e )
		{
			return;
		}
	}

	/**
	 * Provides the user's email address
	 * @return the email address for this user
	 */
	public final String getEmail()
	{
		return email;
	}

	/**
	 * Sets the user's email address
	 * @param str the new email address
	 */
	public final void setEmail(String str)
	{
		email = str;
	}

	/**
	 * Provides the number of seconds which have elapsed since the user
	 * last entered a command.
	 * @return the idle time in seconds
	 */
	public final int getIdle()
	{
		return (int)(((new Date()).getTime() - last_command_time.getTime())/1000);
	}

	/**
	 * Provides the key name for this user.  The key name is a
	 * play on the user name to create a unique identifier for this
	 * user that will always work.  For example, the following
	 * command should work for a user:
	 * 
	 * tell descartes hi!
	 * tell deScartes hi!
	 * tell des cartes hi!
	 *
	 * The key name thus creates a common denomenator to which a name
	 * can be reduced for comparison.
	 * @see #createKeyName
	 * @return the key name
	 */
	public final String getKeyName()
	{
		return key_name;
	}

	/**
	 * Sets the key name during user creation.  This prevents resetting
	 * of the key name.
	 * @param str the key name being set
	 * @see #getKeyName
	 */
	protected void setKeyName(String str)
	{
		if( key_name != null )
		{
			return;
		}
		key_name = str;
	}

	/**
	 * Provides the name of the site from which the user logged in
	 * at their last login.
	 * @return the last login site
	 */
	public final String getLastLoginSite()
	{
		return last_login_site;
	}

	/**
	 * Sets the last login site.  Used by a subclass
	 * during login.
	 * @param site the last login site
	 */
	public void setLastLoginSite(String site)
	{
		if( last_login_site != null )
		{
			return;
		}
		last_login_site = site;
	}

	/**
	 * Provides the time of the user's last login.
	 * @return the last login time
	 */
	public final Date getLastLoginTime()
	{
		return last_login_time;
	}

	/**
	 * Used by the login process to set the last login
	 * time.
	 * @param time the time the user last logged in
	 */
	public void setLastLoginTime(Date time)
	{
		if( last_login_time != null )
		{
			return;
		}
		last_login_time = time;
	}

	/**
	 * Gives the user object's object id.
	 * @return the object id
	 * @see com.planet_ink.coffee_mud.core.intermud.i3.server.ServerObject#getObjectId
	 */
	@Override
	public final String getObjectId()
	{
		return object_id;
	}

	/**
	 * Allows the server to set the object id.
	 * @param id the object id assigned to this object
	 * @see com.planet_ink.coffee_mud.core.intermud.i3.server.ServerObject#setObjectId
	 */
	@Override
	public final void setObjectId(String id)
	{
		if( object_id != null )
		{
			return;
		}
		object_id = id;
	}

	/**
	 * Allows a subclass to get the password.
	 * @return the user's password
	 */
	protected String getPassword()
	{
		return password;
	}

	/**
	 * Sets the user's password.
	 * @param pass the new password
	 */
	protected void setPassword(String pass)
	{
		password = pass;
	}

	/**
	 * Provides the user's command prompt.
	 * @return the command prompt
	 */
	public String getPrompt()
	{
		return "> ";
	}

	/**
	 * Provides the user's real name, or null if they never entered
	 * a real name.
	 * @return the user's real name or null
	 */
	public final String getRealName()
	{
		return real_name;
	}

	/**
	 * Sets the user's real name.
	 * @param nom the real name for the user
	 */
	public void setRealName(String nom)
	{
		real_name = nom;
	}

	/**
	 * Called by the server before connect() is called to assign
	 * the socket for this Interactive to it.
	 * @param s the socket for this connection
	 * @see com.planet_ink.coffee_mud.core.intermud.i3.server.ServerUser#setSocket
	 */
	@Override
	public final void setSocket(Socket s) throws java.io.IOException
	{
		socket = s;
		input_thread = new InputThread(socket, this);
		output_stream = new PrintStream(s.getOutputStream());
	}
}

/**
 * The InputThread class handles asynchronous user input and queues
 * it up to be picked up by the user synchronously.  In English,
 * the user can be entering information at any point in time
 * while the server is running.  You want, however, that a command
 * be executed in a specific order.  This class therefore stuffs commands
 * into a queue when they arrive.  When the user is ready, it pulls a
 * single command off to be executed.
 * Created: 27 September 1996
 * Last modified 27 September 1996
 * @author George Reese (borg@imaginary.com)
 * @version 1.0
 * @see com.planet_ink.coffee_mud.core.intermud.i3.net.Interactive
 */
@SuppressWarnings({"unchecked","rawtypes"})
class InputThread implements Runnable
{
	private final List<String>  			input_buffer;
	private final BufferedReader 			stream;
	private boolean 				destructed;
	private final Thread  				thread;
	private final Interactive 			user;
	private volatile long			internalSize=0;

	/**
	 * Constructs and starts the thread which accepts user
	 * input.  As a user enters a command, the command is
	 * added to a input_buffer.  During each server cycle, the
	 * Interactive object for this thread pulls off one
	 * command and executes it.
	 * @exception java.io.IOException thrown if no input stream can be created
	 * @param s the socket connected to the user's machine
	 * @param u the Interactive attached to this thread
	 */
	public InputThread(Socket s, Interactive u) throws java.io.IOException {
		destructed = false;
		user = u;
		input_buffer = new Vector(10);
		stream = new java.io.BufferedReader(new java.io.InputStreamReader(s.getInputStream()));
		thread = new Thread(Thread.currentThread().getThreadGroup(),this,"I3_"+Thread.currentThread().getName());
		thread.setDaemon(true);
		thread.start();
	}

	/**
	 * As long as the user is connected, this thread accepts
	 * input from the user machine.  If the user drops link,
	 * this will call loseLink() in the interactive object.
	 * @see com.planet_ink.coffee_mud.core.intermud.i3.net.Interactive#loseLink
	 */
	@Override
	public void run()
	{
		while( !destructed )
		{
			String msg;

			try
			{
				msg = stream.readLine();
			}
			catch( final java.io.IOException e )
			{
				synchronized( user )
				{
					user.loseLink();
				}
				return;
			}
			synchronized( this )
			{
				if(msg != null)
				{
					input_buffer.add(msg);
					internalSize+=(msg.length()*2);
				}
			}
			if(internalSize > (10 * 1024 * 1024))
			{
				Log.errOut("Excessive buffer size: "+internalSize);
			}
			try { Thread.sleep(10); }
			catch( final InterruptedException e ) { }
		}
	}

	/**
	 * The interactive object for this input thread will
	 * call stop if the interactive is destructed for
	 * any reason.
	 */
	public void stop()
	{
		destructed = true;
		CMLib.killThread(thread,500,1);
		input_buffer.clear();
	}

	protected synchronized String nextMessage()
	{
		String msg;

		synchronized( input_buffer )
		{
			if( input_buffer.size() > 0 )
			{
				msg = input_buffer.remove(0);
				internalSize-=(msg.length()*2);
			}
			else
			{
				msg = null;
			}
		}
		return msg;
	}
}


